﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    public class Verifier : IVerifier
    {
        private readonly IDependencyModel _dependencyModel;

        public Verifier(IDependencyModel dependencyModel)
        {
            _dependencyModel = dependencyModel;
        }

        public void VerifyForeignModules(IEnumerable<string> allowedForeigners)
        {
            var allowedModules = allowedForeigners as string[] ?? allowedForeigners.ToArray();
            VerifyModules(allowedModules, true);
            VerifyModulesAreUsed(allowedModules);
        }

        private void VerifyModulesAreUsed(IEnumerable<string> modules)
        {
            var usedModules = (from x in _dependencyModel.DependenciesPerModule.Keys select x.FullName).ToArray();
            var violations = modules.Where(module => !usedModules.Contains(module)).ToArray();
            if (violations.Length == 0) return;
            VerifierUtilities.ThrowDependencyException(violations, "Unreferenced module: {0}");
        }

        private void VerifyModules(string[] allowedModules, bool foreigners)
        {
            var modules = _dependencyModel.DependenciesPerModule.Keys;
            var violations = FindViolatingModules(allowedModules, foreigners, modules);
            if (violations.Count == 0) return;

            var message = new StringBuilder();
            foreach (var module in violations.Keys)
            {
                var referrers = violations[module];
                foreach (var referrer in referrers)
                {
                    message.AppendLine(string.Format("Forbidden module: {0} refers to {1}", referrer.FullName,
                                                     module.FullName));
                }
            }
            throw new DependencyException(message.ToString());
        }

        private Dictionary<ModuleDescriptor, ISet<TypeDescriptor>> FindViolatingModules(string[] allowedModules, bool foreigners, ICollection<IDescriptor> modules)
        {
            var violations = new Dictionary<ModuleDescriptor, ISet<TypeDescriptor>>();
            foreach (ModuleDescriptor module in modules)
            {
                if (module.IsPartOfProject || foreigners && module.IsPartOfFramework || !foreigners && module.IsPartOfFramework)
                    continue;
                if (allowedModules.Contains(module.FullName)) continue;
                ISet<TypeDescriptor> referrers;
                if (violations.ContainsKey(module))
                {
                    referrers = violations[module];
                }
                else
                {
                    referrers = new HashSet<TypeDescriptor>();
                    violations[module] = referrers;
                }
                foreach (var dependency in _dependencyModel.DependenciesPerModule[module])
                {
                    if (referrers.Contains(dependency.Source.ImplementingType)) continue;
                    referrers.Add(dependency.Source.ImplementingType);
                }
            }
            return violations;
        }

        public void VerifyForeignModules(IEnumerable<Assembly> allowedForeigners)
        {
            VerifyForeignModules((from x in allowedForeigners select x.FullName).ToArray());
        }

        public void VerifyFrameworkModules(IEnumerable<Assembly> allowedFrameworkModules)
        {
            VerifyFrameworkModules((from x in allowedFrameworkModules select x.FullName).ToArray());
        }

        public void VerifyFrameworkModules(IEnumerable<string> allowedFrameworkModules)
        {
            var allowedModules = allowedFrameworkModules as string[] ?? allowedFrameworkModules.ToArray();
            VerifyModules(allowedModules, false);
        }

        public void VerifySubmodulesInModule(ModuleDescriptor module, IEnumerable<SubmoduleDescriptor> allowedSubmodules)
        {
            if (!_dependencyModel.DependenciesPerModule.ContainsKey(module))
            {
                throw new ArgumentException(string.Format("module '{0}' is not part of the dependency model", module));
            }
            var storedModule = (ModuleDescriptor)_dependencyModel.DependenciesPerModule.FirstOrDefault(x => Equals(x.Key, module)).Key;
            var requestedSubmodules = new HashSet<SubmoduleDescriptor>();
            foreach (var submodule in allowedSubmodules)
            {
                requestedSubmodules.Add(submodule);
            }
            var violations = new List<SubmoduleDescriptor>();
            foreach (var submodule in storedModule.Submodules)
            {
                if (requestedSubmodules.Contains(submodule))
                {
                    requestedSubmodules.Remove(submodule);
                    continue;
                }
                violations.Add(submodule);
            }
            if (violations.Count > 0)
            {
                VerifierUtilities.ThrowDependencyException(violations, "Unapproved submodule: {0}");
            }
            violations.Clear();
            violations.AddRange(requestedSubmodules);
            if (violations.Count > 0)
            {
                VerifierUtilities.ThrowDependencyException(violations, "Submodule does not exist: {0}");
            }
        }

        public void VerifyAccessType<T>(IDescriptor referrer, T target, AbstractionType abstractionType) where T : class, IDescriptor
        {
            if (target == null) throw new ArgumentNullException("target");
            if (referrer == null) throw new ArgumentNullException("referrer");
            var sourceTypes = new HashSet<TypeDescriptor>();
            VerifierUtilities.SelectTypes(_dependencyModel, referrer, sourceTypes);
            var allDependencies = DependencyModelConverter.GetDependencyStructureIn<T>(_dependencyModel);
            if (!allDependencies.ContainsKey(target)) return;
            var violations = (from x in allDependencies[target]
                              where ((int)abstractionType & (int)x.Target.ImplementingType.AbstractionType) == 0
                                    && sourceTypes.Contains(x.Source.ImplementingType)
                              select x.Source).ToArray();
            if (violations.Length == 0) return;
            VerifierUtilities.ThrowDependencyException(violations, "Forbidden accessor: {0}");
        }

        public bool RefersTo(Type referrer, Type target)
        {
            return RefersTo(DescriptorBuilder.BuildTypeDescriptor(referrer), DescriptorBuilder.BuildTypeDescriptor(target));
        }

        public bool RefersTo(Assembly referrer, Assembly target)
        {
            return RefersTo(DescriptorBuilder.BuildModuleDescriptor(referrer), DescriptorBuilder.BuildModuleDescriptor(target));
        }

        public bool RefersTo<T>(IDescriptor referrer, T target) where T : class, IDescriptor
        {
            if (referrer == null) throw new ArgumentNullException("referrer");
            if (target == null) throw new ArgumentNullException("target");
            var dependencyStructureIn = DependencyModelConverter.GetDependencyStructureIn<T>(_dependencyModel);
            if (dependencyStructureIn != null)
            {
                return RefersTo(referrer, target, dependencyStructureIn).Count > 0;
            }
            throw new ArgumentException(string.Format("The type {0} is not supported for parameter target", target.GetType().Name));
        }

        private static IList<DependencyDescriptor> RefersTo<TDescriptor>(IDescriptor referrer, TDescriptor target, IDictionary<TDescriptor, ISet<DependencyDescriptor>> allDependencies) where TDescriptor : IDescriptor
        {
            Func<DependencyDescriptor, IDescriptor, bool> compareFunction = VerifierUtilities.CompareToType;
            if (referrer is TypeDescriptor)
            {
                var typed = (TypeDescriptor)referrer;
                if (typed.OriginatingType != null)
                {
                    referrer = typed.OriginatingType;
                }
                compareFunction = VerifierUtilities.CompareToType;
            }
            else if (referrer is SubmoduleDescriptor) compareFunction = VerifierUtilities.CompareToNamespace;
            else if (referrer is ModuleDescriptor) compareFunction = VerifierUtilities.CompareToAssembly;
            else if (referrer is MemberDescriptor) compareFunction = VerifierUtilities.CompareToMember;
            if (allDependencies.ContainsKey(target))
            {
                var dependencies = allDependencies[target];
                return (from x in dependencies where compareFunction(x, referrer) select x).ToList();
            }
            throw new DependencyException(string.Format("The target {0} is not part of the dependency graph", target.FullName));
        }

        public void VerifyDoesNotReferTo<T>(IDescriptor referrer, T target) where T : class, IDescriptor
        {
            if (referrer == null) throw new ArgumentNullException("referrer");
            if (target == null) throw new ArgumentNullException("target");
            var dependencyStructureIn = DependencyModelConverter.GetDependencyStructureIn<T>(_dependencyModel);
            if (dependencyStructureIn != null)
            {
                var forbiddenDependencies = RefersTo(referrer, target, dependencyStructureIn);
                var violations = VerifierUtilities.AggregateDependenciesToTypes(forbiddenDependencies, true);
                var typeDescriptors = violations as TypeDescriptor[] ?? violations.ToArray();
                if (!typeDescriptors.Any())
                {
                    return;
                }
                VerifierUtilities.ThrowDependencyException(forbiddenDependencies, "Forbidden accessor: {0}");
            }
            throw new ArgumentException(string.Format("The type {0} is not supported for parameter target", target.GetType().Name));
        }

        public void VerifyNoOtherReferrers<T>(IEnumerable<IDescriptor> referrers, T target) where T : class, IDescriptor
        {
            if (referrers == null) throw new ArgumentNullException("referrers");
            if (target == null) throw new ArgumentNullException("target");
            var allowedReferrers = new HashSet<string>();
            AggegateAllowedReferrers<T>(referrers, allowedReferrers);

            var dependencyStructureIn = DependencyModelConverter.GetDependencyStructureIn<T>(_dependencyModel);
            ICollection<DependencyDescriptor> dependencies = null;
            if (dependencyStructureIn.ContainsKey(target))
            {
                dependencies = dependencyStructureIn[target].ToArray();
            }
            if (dependencies == null) return;

            var violations = (from x in dependencies where !allowedReferrers.Contains(x.Source.Identifier) select x.Source).ToArray();
            if (violations.Length == 0) return;

            VerifierUtilities.ThrowDependencyException(violations, "Forbidden accessor: {0}");
        }

        private void AggegateAllowedReferrers<T>(IEnumerable<IDescriptor> referrers, HashSet<string> allowedReferrers) where T : class, IDescriptor
        {
            foreach (var referrer in referrers)
            {
                var member = referrer as MemberDescriptor;
                if (member != null)
                {
                    allowedReferrers.Add(member.Identifier);
                    continue;
                }
                var type = referrer as TypeDescriptor;
                if (type != null)
                {
                    if (type.OriginatingType != null)
                    {
                        type = type.OriginatingType;
                    }
                    (from x in _dependencyModel.DependenciesPerMember
                     where ((MemberDescriptor)x.Key).ImplementingType.Identifier == type.Identifier
                     select x.Key).ToList().ForEach(x => allowedReferrers.Add(x.Identifier));
                    continue;
                }
                var namespaceDescriptorRef = referrer as SubmoduleDescriptor;
                if (namespaceDescriptorRef != null)
                {
                    (from x in _dependencyModel.DependenciesPerMember
                     where ((MemberDescriptor)x.Key).ImplementingType.Submodule.Identifier == namespaceDescriptorRef.Identifier
                     select x.Key).ToList().ForEach(x => allowedReferrers.Add(x.Identifier));
                    continue;
                }
                var module = referrer as ModuleDescriptor;
                if (module != null)
                {
                    (from x in _dependencyModel.DependenciesPerMember
                     where ((MemberDescriptor)x.Key).ImplementingType.Submodule.Module.Identifier == module.Identifier
                     select x.Key).ToList().ForEach(x => allowedReferrers.Add(x.Identifier));
                }
            }
        }

        public void VerifyVersatilityForProject(IEnumerable<IDescriptor> allowedExceptions)
        {
            if (allowedExceptions == null) throw new ArgumentNullException("allowedExceptions");
            var exceptionTypes = new HashSet<TypeDescriptor>();
            var descriptors = allowedExceptions as IDescriptor[] ?? allowedExceptions.ToArray();
            if (descriptors.Length > 0)
            {
                foreach (var exception in descriptors)
                {
                    VerifierUtilities.SelectTypes(_dependencyModel, exception, exceptionTypes);
                }
            }
            var types = (from x in _dependencyModel.DependenciesPerType
                         where ((TypeDescriptor)x.Key).Submodule.Module.IsPartOfProject
                             && !exceptionTypes.Contains(x.Key)
                         select x.Key).ToArray();

            var errorMessage = new StringBuilder();
            foreach (TypeDescriptor typeDescriptor in types)
            {
                AppendVerifyVersatilityMessage(typeDescriptor, errorMessage);
            }
            if (errorMessage.Length > 0)
            {
                throw new DependencyException(errorMessage.ToString());
            }
        }

        public void VerifyVersatility(IDescriptor toVerify, IList<IDescriptor> allowedExceptions)
        {
            if (toVerify == null) throw new ArgumentNullException("toVerify");
            if (allowedExceptions == null) throw new ArgumentNullException("allowedExceptions");
            var includedTypes = new HashSet<TypeDescriptor>();
            VerifierUtilities.SelectTypes(_dependencyModel, toVerify, includedTypes);
            var exceptionTypes = new HashSet<TypeDescriptor>();
            if (allowedExceptions.Count > 0)
            {
                foreach (var exception in allowedExceptions)
                {
                    VerifierUtilities.SelectTypes(_dependencyModel, exception, exceptionTypes);
                }
            }
            var types = (from x in _dependencyModel.DependenciesPerType
                         where ((TypeDescriptor)x.Key).Submodule.Module.IsPartOfProject
                         && includedTypes.Contains(x.Key)
                             && !exceptionTypes.Contains(x.Key)
                         select x.Key).ToArray();

            var errorMessage = new StringBuilder();
            foreach (TypeDescriptor typeDescriptor in types)
            {
                AppendVerifyVersatilityMessage(typeDescriptor, errorMessage);
            }
            if (errorMessage.Length > 0)
            {
                throw new DependencyException(errorMessage.ToString());
            }
        }

        public void VerifyVersatility(TypeDescriptor type)
        {
            if (type.IsGenerated) return;
            var errorMessage = new StringBuilder();
            if (!_dependencyModel.DependenciesPerType.Keys.Contains(type))
            {
                errorMessage.AppendLine(string.Format("the type {0} cannot be found", type.FullName));
            }
            else
            {
                AppendVerifyVersatilityMessage(type, errorMessage);
            }
            if (errorMessage.Length > 0)
            {
                throw new DependencyException(errorMessage.ToString());
            }
        }

        private void AppendVerifyVersatilityMessage(TypeDescriptor type, StringBuilder errorMessage)
        {
            if (!_dependencyModel.DependenciesPerType.ContainsKey(type))
            {
                errorMessage.AppendLine(string.Format("the type {0} cannot be found", type.FullName));
                return;
            }
            var dependencies = new List<DependencyDescriptor>();
            dependencies.AddRange(_dependencyModel.DependenciesPerType[type]);
            if (type.ImplementingTypes != null)
            {
                foreach (var implementer in type.ImplementingTypes)
                {
                    dependencies.AddRange(_dependencyModel.DependenciesPerType[implementer]);
                }
            }
            if (dependencies.Count == 0)
            {
                errorMessage.AppendLine(string.Format("the type {0} is not referenced", type.FullName));
                return;
            }
            var referencingTypes = new HashSet<TypeDescriptor>();
            foreach (var dependency in dependencies)
            {
                if (referencingTypes.Contains(dependency.Source.ImplementingType)) continue;
                referencingTypes.Add(dependency.Source.ImplementingType);
                if (referencingTypes.Count > 1) break;
            }
            if (referencingTypes.Count == 1)
            {
                errorMessage.AppendLine(string.Format("the type {0} is only referenced by {1}", type.FullName,
                                                            referencingTypes.First()));
            }
        }

        public void VerifyMembersAreUsed(IDescriptor target, NodeVisibility visibility)
        {
            var members = new HashSet<MemberDescriptor>();
            VerifierUtilities.SelectMembers(_dependencyModel, target, members);
            var violations = (from member in members.Where(
                                  x => !x.IsGenerated && !x.IsOverride && !x.IsDefinedByInterface && x.MemberType != MemberType.TypeReference &&
                                      (visibility == NodeVisibility.Ignore || ((int)x.Visibility & (int)visibility) != 0) &&
                                      x.Signature != "Void .ctor()" &&
                                      x.Signature != "Void .cctor()")
                              let dependencies = _dependencyModel.DependenciesPerMember[member]
                              where dependencies.Count == 0
                              select member).ToArray();
            if (violations.Length == 0) return;
            VerifierUtilities.ThrowDependencyException(violations, "Member {0} is not referenced.");
        }

        public void VerifyThat<T>(Func<T, State, bool> assumption) where T : IDescriptor
        {
            VerifyThat(assumption, x => true);
        }

        public void VerifyThat<T>(Func<T, State, bool> assumption, Predicate<T> whereClause) where T : IDescriptor
        {
            var dependencyStructure = DependencyModelConverter.GetDependencyStructureIn<T>(_dependencyModel);
            bool violationFound = false;
            var violations = new StringBuilder();
            foreach (T x in dependencyStructure.Keys)
            {
                if (!whereClause(x)) continue;
                var state = new State();
                if (assumption(x, state)) continue;
                violationFound = true;
                violations.AppendLine(x.FullName + " violates assumption " + (string.IsNullOrEmpty(state.Message) ? string.Empty : state.Message));
            }
            if (!violationFound) return;
            throw new DependencyException(violations.ToString());
        }

        public IList<Tuple<T, State>> Report<T>(Func<T, State, bool> metric) where T : IDescriptor
        {
            return Report(metric, x => true);
        }

        public IList<Tuple<T, State>> Report<T>(Func<T, State, bool> metric, Predicate<T> whereClause) where T : IDescriptor
        {
            var dependencyStructure = DependencyModelConverter.GetDependencyStructureIn<T>(_dependencyModel);
            var list = new List<Tuple<T, State>>();
            foreach (T x in dependencyStructure.Keys)
            {
                if (!whereClause(x)) continue;
                var state = new State();
                var doReport = metric(x, state);
                if (!doReport) continue;
                list.Add(new Tuple<T, State>(x, state));
            }
            return list;
        }
    }
}
